package org.groovymud.object.registry;

import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;

import org.apache.log4j.Logger;
import org.groovymud.engine.event.EventScope;
import org.groovymud.engine.event.HeartBeatListener;
import org.groovymud.engine.event.MudEventListener;
import org.groovymud.engine.event.ScopedEvent;
import org.groovymud.engine.event.action.SaveEvent;
import org.groovymud.engine.event.messages.MessageEvent;
import org.groovymud.engine.event.observer.Observable;
import org.groovymud.engine.event.observer.Observer;
import org.groovymud.object.Container;
import org.groovymud.object.MudObject;
import org.groovymud.object.ObjectLocation;
import org.groovymud.object.alive.Alive;
import org.groovymud.object.alive.Player;
import org.groovymud.object.room.Room;
import org.groovymud.shell.Shell;
import org.groovymud.shell.command.CommandInterpreter;
import org.groovymud.shell.telnetd.LoggingExtendedTerminalIO;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

/* Copyright 2008 Matthew Corby-Eaglen
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0 
 *
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License. 
 */
/**
 * The registry is the "root" container for all mud objects all global scoped
 * events pass through this object, and in turn are (if necessary) passed down
 * to the child rooms and objects.
 * 
 * spring injects mudObjectAttendant and commandIterpreter
 * 
 * this object is spring applicationcontextaware and this will get injected by
 * spring automatically.
 */
public class Registry implements Container, Observer, ApplicationContextAware {

	private final static Logger logger = Logger.getLogger(Registry.class);

	private final Map<Shell, Player> activePlayerHandles;

	protected final InventoryHandler inventoryHandler;
	private final transient CommandInterpreter commandInterpreter;

	private transient ApplicationContext applicationContext;

	private String mudName;

	private transient Set<HeartBeatListener> heartBeatListeners;

	public Registry(InventoryHandler globalInventoryHandler, CommandInterpreter commandInterpreter) {
		this.inventoryHandler = globalInventoryHandler;
        this.commandInterpreter = commandInterpreter;
		activePlayerHandles = Collections.synchronizedMap(new WeakHashMap<Shell, Player>());
		heartBeatListeners = Collections.synchronizedSet(new HashSet<HeartBeatListener>());
	}


	public void register(MudObject obj) {
		register(obj, null);
	}

	/**
	 * registers a parameter with the registry, and re-links all the contents of
	 * any containers.
	 * 
	 * This method has to set the registry param of the registered object
	 * because persistence removes it
	 * 
	 * @param obj
	 */
	public void register(MudObject obj, String handle) {
		obj.setRegistry(this);
        obj.setInterpreter(commandInterpreter);

		if (isContainerInstance(obj)) {
			Container con = castToContainer(obj);
			relinkContent(con);
		}
		if (isHeartBeatListener(obj)) {
			HeartBeatListener hbl = castToHeartBeatListener(obj);
			registerHeartBeatListener(hbl);
		}
		if (isAliveInstance(obj)) {
			Alive alive = castToAlive(obj);
			if (alive.getTerminalOutput() == null) {
                final LoggingExtendedTerminalIO terminalIO = new LoggingExtendedTerminalIO();
                alive.setTerminalOutput(terminalIO);
			    terminalIO.setObject(alive);
			}
		}

		obj.initialise();
		if (handle != null) {
			inventoryHandler.addMudObject(handle, obj);
		}
		addMudObject(obj);
	}

	protected HeartBeatListener castToHeartBeatListener(Object obj) {
		return (HeartBeatListener) obj;
	}

	protected boolean isHeartBeatListener(Object obj) {
		return obj instanceof HeartBeatListener;
	}

	/**
	 * container objects, when reloaded after persistence, loose their observers
	 * and link to the registry. this method relinks them with the game.
	 * 
	 * warning - this method is recursive, and calls its self.
	 * 
	 * @param theContainer
	 */
	public void relinkContent(Container theContainer) {
		Set<MudObject> mudObjectSet = theContainer.getMudObjects();
		synchronized (mudObjectSet) {
			for (MudObject mudObject : mudObjectSet) {
				mudObject.setInterpreter(getCommandInterpreter());
				mudObject.setRegistry(this);

				if (mudObject instanceof Container) {
					Container container = castToContainer(mudObject);
					relinkContent(container);
				}

				mudObject.setCurrentContainer(theContainer); // relink container data
				if (mudObject instanceof Observable) {
					((Observable) mudObject).addObserver(theContainer);
				}
				register(mudObject); // re-register contents
			}
		}
	}

	public void update(MudObject object) {
		if (getMudObjects().contains(object)) {
			removeMudObject(object);
			register(object);
		} else {
			logger.warn("cannot update registry for "+ object);
		}
	}

	protected Alive castToAlive(MudObject obj) {
		return (Alive) obj;
	}

	protected boolean isAliveInstance(MudObject obj) {
		return obj instanceof Alive;
	}

	protected boolean isContainerInstance(MudObject obj) {
		return obj instanceof Container;
	}

	protected Observable castToObservable(MudObject object) {
		return ((Observable) object);
	}

	public void addActivePlayer(Shell shell, Player player) {
		addActivePlayerHandle(shell, player);
		if (logger.isDebugEnabled()) {
			logger.debug("There are " + getActivePlayerHandles().size() + " connections active and " + (Runtime.getRuntime().freeMemory() / 1000) + " kBytes free");
		}
	}

	public void removeActivePlayer(Player player) {
		getActivePlayerHandlesMap().values().remove(player);
	}

	public MudObject removeMudObject(MudObject object) {
		if (object instanceof Player) {
			removeActivePlayer(castToPlayer(object));
		}
		if (object instanceof HeartBeatListener) {
			removeHeartBeatListener(castToHeartBeatListener(object));
		}
		return getInventoryHandler().removeMudObject(object);
	}

	private void addActivePlayerHandle(Shell shell, Player player) {
		getActivePlayerHandlesMap().put(shell, player);
	}

	public Player getPlayerByHandle(Shell handle) {
		return getActivePlayerHandlesMap().get(handle);
	}

	protected Map<Shell, Player> getActivePlayerHandlesMap() {
		return activePlayerHandles;
	}

	protected Observer castToObserver(Object o) {
		return ((Observer) o);
	}

	public void update(Observable object, ScopedEvent arg) {
		for (Object objs : getMudObjects()) {
			if (objs instanceof MudEventListener) {
				castToMudEventListener(objs).onBeforeMudEvent(arg);
			}
		}
		if (!arg.isConsumed()) {
			for (Object objs : getMudObjects()) {
				if (objs instanceof MudEventListener) {
					castToMudEventListener(objs).onMudEvent(arg);
				}
			}
		}
		if (!arg.isConsumed()) {
			for (Object objs : getMudObjects()) {
				if (objs instanceof MudEventListener) {
					castToMudEventListener(objs).onAfterMudEvent(arg);
				}
			}
		}
	}

	private MudEventListener castToMudEventListener(Object objs) {
		return ((MudEventListener) objs);
	}

	public void dest(MudObject object) {
		dest(object, false);
	}

	public void dest(MudObject object, boolean withExtremePrejudice) {
		logger.info("trying to dest:" + object + " wep? " + withExtremePrejudice);
		if (object instanceof Player) {
			Player player = castToPlayer(object);
			fireLeavingMessage(player);

			if (!withExtremePrejudice) {
				fireSave(player);
			}
		}
		if (object instanceof Container) {
			unregisterContents(((Container) object));
		}
		removeMudObject(object);
		if (object.getCurrentContainer() != null) {
			object.getCurrentContainer().removeMudObject(object);
		}
		if (object instanceof Player) {
			try {
				((Player) object).getTerminalOutput().close();
			} catch (IOException e) {
				logger.error(e, e);
			}
		}
	}

	private Player castToPlayer(MudObject object) {
		return (Player) object;
	}

	/**
	 * removes all objects from any container and sub containers
	 * 
	 * eg. "dest" them
	 * 
	 * @param container
	 */
	public void unregisterContents(Container container) {
		for (MudObject obj : container.getInventoryHandler().getMudObjects()) {
			// remove objects from the game, but not from the container
			if (obj instanceof Container) {
				unregisterContents(castToContainer(obj));
			}
			removeMudObject(obj);
		}
	}

	private Container castToContainer(MudObject obj) {
		return ((Container) obj);
	}

	protected void fireLeavingMessage(Player player) {
		MessageEvent leaves = new MessageEvent(EventScope.GLOBAL_SCOPE);
		leaves.setScopeMessage("[" + player.getName() + " leaves " + mudName + "]");
		leaves.setSource(player);
		leaves.setSourceMessage("Thanks for visiting!!");
		try {
			((Observable) player).fireEvent(leaves);
		} catch (Exception e) {
			logger.info("could not send leaving event");
		}
	}

    protected void fireSave(Player player){
        SaveEvent saveEvent = new SaveEvent();
        saveEvent.setSource(player);
        ((Observable)player).fireEvent(saveEvent);
    }

	public void clear() {
		inventoryHandler.clear();
	}


	public boolean contains(MudObject object) {
		return inventoryHandler.contains(object);
	}

	public boolean containsAll(Collection<?> c) {
		return inventoryHandler.containsAll(c);
	}

	public Map<String, Set<MudObject>> getMudObjectsMap() {
		return getInventoryHandler().getMudObjectsMap();
	}

	public HashSet<Shell> getActivePlayerHandles() {
		return new HashSet<Shell>(getActivePlayerHandlesMap().keySet());
	}

	public void addMudObject(MudObject object) {
		if (object instanceof Room) {
			if (object.getCurrentContainer() == null) {
				object.setCurrentContainer(this);
			}
			castToObservable(object).addObserver(this);
		}
		getInventoryHandler().addMudObject(object);
	}

	public MudObject getMudObject(String name) {
		return getInventoryHandler().getMudObject(name);
	}

	public Set<MudObject> getMudObjects(String name) {
		return getInventoryHandler().getMudObjects(name);
	}

	public Set<MudObject> getMudObjects() {
		return getInventoryHandler().getMudObjects();
	}

	public InventoryHandler getInventoryHandler() {
		return inventoryHandler;
	}

	public void setInventoryHandler(InventoryHandler handler) {
		throw new UnsupportedOperationException("Registry cannot have its inventory handler set");
	}

	public EventScope getScope() {
		return EventScope.GLOBAL_SCOPE;
	}

	public ApplicationContext getApplicationContext() {
		return applicationContext;
	}

	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		this.applicationContext = applicationContext;
	}

	public ObjectLocation getObjectLocation() {
		return null;
	}

	public void setScope(EventScope scope) {

	}

	public CommandInterpreter getCommandInterpreter() {
		return commandInterpreter;
	}

	/**
	 * @return the mudName
	 */
	public String getMudName() {
		return mudName;
	}

	/**
	 * @param mudName the mudName to set
	 */
	public void setMudName(String mudName) {
		this.mudName = mudName;
	}

	public void registerHeartBeatListener(HeartBeatListener listener) {
		heartBeatListeners.add(listener);
	}

	public void removeHeartBeatListener(HeartBeatListener listener) {
		heartBeatListeners.remove(listener);
	}

	public Set<HeartBeatListener> getHeartBeatListeners() {
		Set<HeartBeatListener> concreteSet = new HashSet<HeartBeatListener>(heartBeatListeners);
		return concreteSet;
	}

}
